package csm.common.security;

import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Calendar;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;

/**
 * 
 * @author seungmun
 *
 */
public class DESCryptography
{
	private final static String ALGORITHM = "DESede";
	private final static int KEY_LENGTH = 168;
	private final static String PADDING = "DESede/ECB/PKCS5Padding";

	/**
	 * 키값을 생성합니다.
	 * 
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public Key generateKey() throws NoSuchAlgorithmException
	{
		KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
		keyGenerator.init(KEY_LENGTH);
		Key key = keyGenerator.generateKey();
		return key;
	}

	/**
	 * cipher를 생성합니다.
	 * 
	 * @param key
	 * @param mode
	 *            Cipher.ENCRYPT_MODE : encryption, Cipher.DECRYPT_MODE :
	 *            decryption
	 * @return
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 * @throws InvalidKeyException
	 */
	private Cipher createCipher(Key key, int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException
	{
		Cipher cipher = Cipher.getInstance(PADDING);
		cipher.init(mode, key);
		return cipher;
	}

	/**
	 * 암호화 처리를 합니다.
	 * 
	 * @param plainText
	 * @param charSet
	 * @param key
	 * @return
	 * @throws Exception
	 */
	public byte[] encrypt(String plainText, String charSet, Key key) throws Exception
	{
		try
		{
			Cipher cipher = createCipher(key, Cipher.ENCRYPT_MODE);
			byte[] encoded = cipher.doFinal(plainText.getBytes(charSet));
			return encoded;
		}
		catch (Exception e)
		{
			throw new Exception(e.getMessage());
		}
	}

	/**
	 * 복호화 처리를 합니다.
	 * 
	 * @param cipherText
	 * @param charSet
	 * @param key
	 * @return
	 * @throws Exception
	 */
	public String decrypt(byte[] cipherText, String charSet, Key key) throws Exception
	{
		try
		{
			Cipher cipher = createCipher(key, Cipher.DECRYPT_MODE);
			byte[] decoded = cipher.doFinal(cipherText);
			return new String(decoded, charSet);
		}
		catch (Exception e)
		{
			e.printStackTrace(System.err);
			throw new Exception(e.getMessage());
		}
	}

	/**
	 * HEX String으로 변환합니다.
	 * 
	 * @param key
	 * @return
	 */
	public String toHexKey(Key key)
	{
		return byteArrayToHexString(key.getEncoded());
	}

	/**
	 * 암호화 키에 Expire시간을 암호화해서 추가한다.
	 * 
	 * @param key
	 * @param expire
	 *            Expire될 시간 (milisecond단위)
	 * @return
	 */
	public String toHexKeyWithExpireTime(Key key, int expire)
	{
		String keyString = byteArrayToHexString(key.getEncoded());
		StringBuffer sb = new StringBuffer(keyString);
		// Expire시간을 암호화 해서 맨 뒤에 붙인다.
		long expireTime = Calendar.getInstance().getTimeInMillis() + expire;
		String expTime = expireTime + "";
		String timeString;
		try
		{
			timeString = byteArrayToHexString(encrypt(expTime, "UTF-8", key));
			sb.append(timeString).append(padl(timeString.length() + "", 4, '0'));
			return sb.toString();
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return "";
		}
	}

	/**
	 * HEX String에서 Key를 생성합니다.
	 * 
	 * @param hexKey
	 * @return
	 * @throws InvalidKeyException
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeySpecException
	 */
	public Key fromHexKey(String hexKey) throws Exception
	{
		// 입력값을 bytes로 변환
		byte[] desKeyData = hexStringToByteArray(hexKey);
		// 암호화 키 생성
		try
		{
			KeySpec keySpec = new DESedeKeySpec(desKeyData);
			SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
			return keyFactory.generateSecret(keySpec);
		}
		catch (Exception e)
		{
			e.printStackTrace(System.err);
			throw new Exception("잘못된 암호화 키입니다.");
		}
	}

	/**
	 * 암호화 키에 Expire시간을 암호화해서 추가한다.
	 * 
	 * @param key
	 * @param expire
	 *            Expire될 시간 (milisecond단위)
	 * @return
	 */
	public Key fromHexKeyWithExpireTime(String hexKey) throws Exception
	{
		if (hexKey.length() < 4) throw new Exception("잘못된 암호화 키입니다.");
		// 1) 뒤의 4자리를 잘라내서 만료시간이 암호화 된 부분의 길이를 구한다.
		int timeLen = Integer.parseInt(right(hexKey, 4));
		// 2) 암호화된 만료시간을 구한다.
		String expireTime = hexKey.substring(hexKey.length() - (timeLen + 4), hexKey.length() - 4);
		// 3) 원래의 키를 추출한다.
		String originalKey = hexKey.substring(0, hexKey.length() - (timeLen + 4));
		byte[] desKeyData = hexStringToByteArray(originalKey);

		try
		{
			KeySpec keySpec = new DESedeKeySpec(desKeyData);
			SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
			Key key = keyFactory.generateSecret(keySpec);
			// 키의 유효기간을 체크한다.
			long expTime = Long.parseLong(decrypt(hexStringToByteArray(expireTime), "UTF-8", key));
			if (Calendar.getInstance().getTimeInMillis() > expTime) throw new Exception("키 유효기간이 만료되었습니다.");
			return key;
		}
		catch (Exception e)
		{
			e.printStackTrace(System.err);
			throw new Exception("잘못된 암호화 키입니다.");
		}
	}

	public static String byteArrayToHexString(byte[] b)
	{
		StringBuffer sb = new StringBuffer(b.length * 2);
		for (int i = 0; i < b.length; i++)
		{
			int v = b[i] & 0xff;
			if (v < 16)
			{
				sb.append('0');
			}
			sb.append(Integer.toHexString(v));
		}
		return sb.toString().toUpperCase();
	}

	public static byte[] hexStringToByteArray(String s)
	{
		byte[] b = new byte[s.length() / 2];
		for (int i = 0; i < b.length; i++)
		{
			int index = i * 2;
			int v = Integer.parseInt(s.substring(index, index + 2), 16);
			b[i] = (byte) v;
		}
		return b;
	}

	/**
	 * 주어진 길이 만큼 문자열의 좌측을 채워넣는다. 문자열이 지정한 길이보다 길다면 잘라낸다. 넘어온 문자열이 null인 경우에도
	 * 처리되도록 수정.
	 */
	private static String padl(String s, int l, char c)
	{
		if (s == null) s = "";
		StringBuffer sb = new StringBuffer();
		int i = Math.min(s.length(), l);
		sb.append(s.substring(0, i));
		i = l - i;
		while (i > 0)
		{
			sb.insert(0, c);
			i--;
		}
		return sb.toString();
	}

	/**
	 * 문자열의 가장 우측에서 지정된 길이만큼 잘라서 리턴합니다.
	 * 
	 * @param str
	 * @param len
	 * @return
	 */
	public static String right(String str, int len)
	{
		if (str == null)
		{
			return null;
		}
		if (len < 0)
		{
			return "";
		}
		if (str.length() <= len)
		{
			return str;
		}
		else
		{
			return str.substring(str.length() - len);
		}
	}
}
